package cn.seaboot.commons.ftp;

import cn.seaboot.commons.core.CommonUtils;
import cn.seaboot.commons.file.FilenameUtils;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPConnectionClosedException;
import org.apache.commons.net.ftp.FTPFile;
import org.springframework.util.Assert;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

/**
 * {@link FTPClient}的二次封装，此工具设计的目的，是为了更方便地使用它。
 * <p>
 * {@link FTPClient}大部分函数，都是返回一个 boolean 类型的值，false的时候，表示执行失败，
 * 这个类，对所有返回值做了处理，执行失败会抛出{@link IllegalStateException} 异常，
 * 主要是因为，布尔类型的返回值，属于很少出现的情况，很容易被程序员直接忽略，导致最后不做任何处理。
 *
 * @author Mr.css
 * @version 2021-02-24 10:09
 */
public class Ftp implements AutoCloseable {
    /**
     * 连接工厂
     */
    private final DataSource dataSource;
    /**
     * 当前ftp连接
     */
    private final FTPClient ftpClient;
    /**
     * 当前FTP的参数
     */
    private final FtpProperties ftpProperties;
    /**
     * 默认的文件夹
     */
    private final String root;

    public Ftp(DataSource dataSource, FtpProperties ftpProperties) {
        try {
            this.dataSource = dataSource;
            this.ftpProperties = ftpProperties;
            this.ftpClient = dataSource.createFTPClient();
            this.root = this.ftpClient.printWorkingDirectory();
        } catch (IOException e) {
            throw new FtpException("Receive ftp connection failed: " + ftpProperties, e);
        }
    }

    /**
     * Returns an InputStream from which a named file from the server can be read.
     * 结束调用，一定要记得调用：{@link Ftp#completePendingCommand()}.
     *
     * @param remote The name of the remote file.
     * @return InputStream
     * @throws FTPConnectionClosedException 连接长时间空闲、超出ftp连接数上限，都可能会出现返回码421错误，一般需要销毁连接。
     *                                      If the FTP server prematurely closes the connection as a result
     *                                      of the client being idle or some other reason causing the server
     *                                      to send FTP reply code 421.  This exception may be caught either
     *                                      as an IOException or independently as itself.
     * @throws IOException                  -
     */
    public InputStream openInputSteam(String remote) throws IOException {
        return this.ftpClient.retrieveFileStream(remote);
    }

    /**
     * get the transfer response，获取传输响应。
     * 完成一些操作之后（例如：文件传输），需要调用此函数，确保事务已经全部完成。
     * 详见：{@link #retrieveFile(String, OutputStream)}。
     *
     * @return True if successfully completed, false if not.
     * @throws IOException           -
     * @throws IllegalStateException command not completed
     */
    public Ftp completePendingCommand() throws IOException {
        Assert.state(this.ftpClient.completePendingCommand(), "Ftp pending command is not completed!");
        return this;
    }

    /**
     * 存储文件
     *
     * @param fileName 文件名
     * @param is       文件流
     * @return this
     * @throws IOException           -
     * @throws IllegalStateException command not completed
     */
    public Ftp storeFile(String fileName, InputStream is) throws IOException {
        Assert.state(this.ftpClient.storeFile(fileName, is), "Ftp store file is not completed: " + fileName);
        return this;
    }


    /**
     * 下载文件到输出流
     *
     * @param fileName 文件名
     * @param os       输出流
     * @throws IOException           -
     * @throws IllegalStateException command not completed
     */
    public Ftp retrieveFile(String fileName, OutputStream os) throws IOException {
        Assert.state(this.ftpClient.retrieveFile(fileName, os), "Ftp retrieve file is not completed: " + fileName);
        return this;
    }

    /**
     * 返回当前的工作目录
     *
     * @return string 当前文件夹路径
     * @throws IOException -
     */
    public String pwd() throws IOException {
        return this.ftpClient.printWorkingDirectory();
    }

    /**
     * 退回到父级文件夹
     *
     * @return this
     * @throws IOException           -
     * @throws IllegalStateException command not completed
     */
    public Ftp changeToParentDirectory() throws IOException {
        Assert.state(this.ftpClient.changeToParentDirectory(), "Ftp change to parent directory is not completed");
        return this;
    }


    /**
     * 切换工作目录，路径为空的情况下，则不会发生任何事，如果对程序有影响，需要做好判断
     *
     * @param path 文件夹
     * @return this
     * @throws IOException           -
     * @throws IllegalStateException command not completed
     */
    public Ftp cd(String path) throws IOException {
        if (CommonUtils.isEmpty(path)) {
            return this;
        }
        Assert.state(ftpClient.changeWorkingDirectory(path), "Ftp change working directory is not completed: " + path);
        return this;
    }

    /**
     * 创建文件夹
     *
     * @param dir 文件夹名称
     * @return this
     */
    public Ftp mkdir(String dir) throws IOException {
        Assert.state(this.ftpClient.makeDirectory(dir), "Ftp make directory is not completed: " + dir);
        return this;
    }

    /**
     * 重置工作空间，返回到默认的作业目录，这个路径不一定是 "/"
     *
     * @return this
     */
    public Ftp changeToRootDirectory() throws IOException {
        return this.cd(this.root);
    }


    /**
     * 返回当前的工作目录，保留原先的函数名，可能有些人会更习惯 apache 的API
     *
     * @return string 当前文件夹路径
     * @throws IOException -
     */
    public String printWorkingDirectory() throws IOException {
        return this.ftpClient.printWorkingDirectory();
    }

    /**
     * 切换工作目录，保留原先的函数名，可能有些人会更习惯 apache 的API
     *
     * @param path 文件夹
     * @return this
     * @throws IOException              -
     * @throws IllegalArgumentException 路径为空
     * @throws IllegalStateException    切换路径失败
     */
    public Ftp changeWorkingDirectory(String path) throws IOException {
        return this.cd(path);
    }

    /**
     * 创建文件夹，保留原先的函数名，可能有些人会更习惯apache的API
     *
     * @param dir 文件夹名称
     * @return this
     */
    public Ftp makeDirectory(String dir) throws IOException {
        return this.mkdir(dir);
    }

    /**
     * 删除一个文件
     *
     * @return True if successfully completed, false if not.
     */
    public Ftp deleteFile(String filePath) throws IOException {
        Assert.state(ftpClient.deleteFile(filePath), "Ftp delete file not complete: " + filePath);
        return this;
    }

    /**
     * 删除一个空的文件夹
     *
     * @return True if successfully completed, false if not.
     */
    public Ftp removeDirectory(String filePath) throws IOException {
        Assert.state(ftpClient.removeDirectory(filePath), "Ftp remove directory not complete: " + filePath);
        return this;
    }

    /**
     * 遍历当前工作空间的所有文件
     *
     * @return file array
     * @throws IOException           -
     * @throws FileNotFoundException 文件目录为空
     */
    public FTPFile[] listFiles() throws IOException {
        return ftpClient.listFiles();
    }

    /**
     * 遍历当前工作空间的所有文件夹
     *
     * @return file array
     * @throws IOException           -
     * @throws FileNotFoundException 文件目录为空
     */
    public FTPFile[] listDirectories() throws IOException {
        return ftpClient.listDirectories();
    }

    /**
     * 遍历当前工作空间的所有文件
     *
     * @return file array
     * @throws IOException           -
     * @throws FileNotFoundException 文件目录为空
     */
    public String[] listNames() throws IOException {
        return ftpClient.listNames();
    }

    // 二次封装函数 -------------------------------------------------------------------------------------------------------------------------

    /**
     * 删除一个空的文件夹
     *
     * @param filePath 兼容格式："path/name.xml"、"name.xml"，完成之后，会返回到当前路径
     * @return True if successfully completed
     */
    public Ftp delete(String filePath) throws IOException {
        String name = FilenameUtils.getName(filePath);
        if (name.length() == filePath.length()) {
            return this.deleteFile(name);
        } else {
            String path = filePath.substring(0, filePath.length() - name.length());
            String current = this.pwd();
            try {
                return this.cd(path).deleteFile(name);
            } finally {
                // 返回当前路径
                this.cd(current);
            }
        }
    }

    /**
     * 递归清空并删除文件夹，删除完成之后，会返回到当前路径
     *
     * @param filePath 兼容格式："path/name.xml"、"name。xml"，完成之后，会返回到当前路径
     * @throws IOException -
     */
    public void clearDirectory(String filePath) throws IOException {
        String current = this.pwd();
        try {
            this.cd(filePath);
            // 删除所有文件
            FTPFile[] files = this.listFiles();
            for (FTPFile ftpFile : files) {
                this.ftpClient.deleteFile(ftpFile.getName());
            }
            // 递归检索文件夹
            files = this.listDirectories();
            for (FTPFile ftpFile : files) {
                this.clearDirectory(ftpFile.getName());
                this.ftpClient.changeToParentDirectory();
            }
            this.ftpClient.changeToParentDirectory();
            this.ftpClient.removeDirectory(filePath);
        } finally {
            // 返回当前路径
            this.cd(current);
        }
    }

    /**
     * 文件上传
     *
     * @param path       路径
     * @param fileName   文件名
     * @param fileStream 文件IO
     * @return this
     * @throws IOException -
     */
    public Ftp upload(String path, String fileName, InputStream fileStream) throws IOException {
        String current = this.pwd();
        try {
            return this.cd(path).storeFile(fileName, fileStream);
        } finally {
            this.cd(current);
        }
    }

    /**
     * 文件上传
     *
     * @param filePath   兼容格式："path/name.xml"、"name.xml"，完成之后，会返回到当前路径
     * @param fileStream 文件IO
     * @return this
     * @throws IOException -
     */
    public Ftp upload(String filePath, InputStream fileStream) throws IOException {
        String name = FilenameUtils.getName(filePath);
        if (name.length() == filePath.length()) {
            return this.storeFile(name, fileStream);
        } else {
            String path = filePath.substring(0, filePath.length() - name.length());
            return this.upload(path, name, fileStream);
        }
    }


    /**
     * 下载文件到输出流
     *
     * @param path     文件路径
     * @param fileName 文件名
     * @param out      输出位置
     * @return this
     * @throws IOException -
     */
    public Ftp download(String path, String fileName, OutputStream out) throws IOException {
        String current = this.pwd();
        try {
            return this.cd(path).retrieveFile(fileName, out);
        } finally {
            this.cd(current);
        }
    }


    /**
     * 下载文件到输出流
     *
     * @param filePath 文件路径，可以包含文件名
     * @param out      输出位置
     * @return this
     * @throws IOException -
     */
    public Ftp download(String filePath, OutputStream out) throws IOException {
        String name = FilenameUtils.getName(filePath);
        if (name.length() == filePath.length()) {
            return this.retrieveFile(name, out);
        } else {
            String path = filePath.substring(0, filePath.length() - name.length());
            return this.download(path, name, out);
        }
    }

    /**
     * 退还连接到连接池中
     */
    @Override
    public void close() {
        dataSource.closeFtp(this);
    }

    /**
     * 返回当前的ftp实例
     *
     * @return FTPClient
     */
    public FTPClient getFtpClient() {
        return ftpClient;
    }

    /**
     * 返回当前的ftp运行参数
     *
     * @return FTPClient
     */
    public FtpProperties getFtpProperties() {
        return ftpProperties;
    }
}
